/* **********************************************************************************
 * Copyright (c) 2011 John Hughes
 *
 * Elve Touch is licenced under the Microsoft Reciprocal License (Ms-RL).
 *
 * Project Website: http://elvetouch.codeplex.com/
 * **********************************************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.IO;
using System.Net;
using ElveTouch.Properties;

namespace ElveTouch
{
    class ImageCache
    {
        public static bool AllowHardDriveCache = true;

        class ImageContainer : IDisposable
        {
            public string ImagePath;
            public int ImageHash;
            public Image Image;
            public int ByteLength;

            public ImageContainer(string imagePath, int imageHash, Image img, int byteLength)
            {
                this.ImagePath = imagePath;
                this.ImageHash = imageHash;
                this.Image = img;
                this.ByteLength = byteLength;
            }

            public string Key
            {
                get { return CreateKey(ImagePath, ImageHash); }
            }

            public static string CreateKey(string imagePath, int imageHash)
            {
                return imagePath + "|" + imageHash.ToString();
            }

            public void Dispose()
            {
                if (Image != null)
                {
                    Image.Dispose();
                    Image = null;
                }
            }
        }

        private static NamedIndexedDictionary<ImageContainer> _images = new NamedIndexedDictionary<ImageContainer>(true, true, false);

        private static string _hardDriveCacheDirectoryPath;
        private static bool _isMasterServerPath;

        private static int _maxCacheMemoryBytes = 1024 * 1024 * 200;
        private static int _currentCacheMemoryBytes = 0;

        static ImageCache()
        {
            // Determine if we are on the master server. If so then do not cache, and just use the master server's TouchScreenImages to get images.
            string masterServerImagePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), @"Codecore Technologies\Elve\TouchScreenImages"); ;
            if (Directory.Exists(masterServerImagePath))
            {
                _hardDriveCacheDirectoryPath = masterServerImagePath;
                _isMasterServerPath = true;
            }
            else
            {
                // Windows Vista path example: "C:\ProgramData\Codecore Technologies\Elve\Cache\TouchScreenImages"
                _hardDriveCacheDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), @"Codecore Technologies\Elve\Cache\TouchScreenImages");
                _isMasterServerPath = false;
            }
        }

        public static Image GetImage(string imagePath, int imageHash)
        {
            string key = ImageContainer.CreateKey(imagePath, imageHash);

            //***********************************************************
            // Check if the image is already in the memory cache.
            //***********************************************************
            if (_images.ContainsKey(key))
            {
                // Move to top of list
                _images.AddAt(0, key, _images[key]); // yes, use AddAt() because we told the class to ReplaceDuplicateKeys so it will just move it.
                // return cached item
                return _images[key].Image;
            }

            //***********************************************************
            // Check if the image is in the hard drive cache.
            //***********************************************************
            if ((AllowHardDriveCache || _isMasterServerPath) && imagePath.StartsWith("IMAGE://", StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    string path = Path.Combine(_hardDriveCacheDirectoryPath, imagePath.Substring(8));
                    if (File.Exists(path))
                    {
                        // Do not use Image.FromFile() so that we don't lock the file.  In case we need to clear the cache later (which attempts to delete the files)

                        byte[] b;
                        b = File.ReadAllBytes(path);
                        Stream fromFileStream = new MemoryStream(b); // DO NOT DISPOSE THE STREAM!!! See below.
                        Image img = Image.FromStream(fromFileStream); // DO NOT DISPOSE THE STREAM!!! See below.

                        int imgFileHash = (Int32)SystemUtility.CalculateUInt32Checksum(b);

                        if (imageHash == imgFileHash)
                        {
                            ImageContainer imgContainer = new ImageContainer(imagePath, imageHash, img, b.Length);
                            addImageContainerToCache(imgContainer);
                            return img;
                        }
                    }
                }
                catch (Exception ex)
                {
                    // do nothing... it will be loaded below.
                }
            }
            else if (imagePath.StartsWith("HTTP://", StringComparison.OrdinalIgnoreCase))
            {
                // Read image from web.
                try
                {
                    using (WebClient client = new WebClient())
                    {
                        byte[] b = client.DownloadData(imagePath);
                        Stream stream = new MemoryStream(b); // DO NOT DISPOSE THE STREAM!!! See below.

                        Image img = Image.FromStream(stream); // DO NOT DISPOSE THE STREAM!!! See below.

                        return img;
                    }
                }
                catch (Exception ex)
                {
                    return Resources.BadImage;  // failed to get image
                }
            }

            return null;
        }

        private static void addImageContainerToCache(ImageContainer imgContainer)
        {
            // At the image at the beginning of the cache... TODO: it might be better to use a "most often used" algorithm as oppoed to a "most recently used" algorithm.
            _images.AddAt(0, imgContainer.Key, imgContainer); // store image in cache

            // Increment the byte size of the total memory cache.
            _currentCacheMemoryBytes += imgContainer.ByteLength;

            // Remove items from the end of the cache until we are under the max cache size.
            while (_currentCacheMemoryBytes > _maxCacheMemoryBytes)
            {
                // Remove and get oldest cached item.
                ImageContainer removeContainer = _images.RemoveAt(_images.Count - 1);
                // Decrement the byte size of the total memory cache.
                _currentCacheMemoryBytes -= removeContainer.ByteLength;
                // Dispose the image container.
                removeContainer.Dispose();
            }
        }

        /// <summary>
        /// Removes an image from the memory cache.
        /// </summary>
        /// <param name="location"></param>
        public static void RemoveImage(string imagePath)
        {
            // Remove the existing image by name (without hash).

            ImageContainer imgContainer = findImageContainerByPath(imagePath);

            if (imgContainer != null)
            {
                _images.Remove(imgContainer.Key);
                imgContainer.Dispose();
                _currentCacheMemoryBytes -= imgContainer.ByteLength;
            }

            if (AllowHardDriveCache && !_isMasterServerPath && imagePath.StartsWith("IMAGE://", StringComparison.OrdinalIgnoreCase))
            {
                //*******************************************************
                // Delete the image file.
                //*******************************************************
                string path = Path.Combine(_hardDriveCacheDirectoryPath, imagePath.Substring(8));
                if (File.Exists(path))
                    File.Delete(path);
            }
        }

        /// <summary>
        /// Clear the cache.
        /// </summary>
        public static void ClearCache()
        {
            try
            {
                // Clear the memory cache
                //_images.Clear(); Don't call this! we need to dispose the images!!!
                // Get a copy of images and create an empty one before disposing to prevent a threading race condition.
                NamedIndexedDictionary<ImageContainer > images = _images;
                _images = new NamedIndexedDictionary<ImageContainer>(true, true, false);
                foreach (ImageContainer ic in _images.Values)
                    ic.Dispose();

                if (AllowHardDriveCache && !_isMasterServerPath)
                {
                    // Delete the cache directory on the hard drive
                    Directory.Delete(_hardDriveCacheDirectoryPath, true);
                }
            }
            catch (Exception ex)
            {
                // do nothing.
            }
        }


        

        static ImageContainer findImageContainerByPath(string imagePath)
        {
            // returns null if not found

            foreach (ImageContainer item in _images.Values)
            {
                if (item.ImagePath.Equals(imagePath, StringComparison.OrdinalIgnoreCase))
                {
                    return item;
                }
            }

            return null;
        }

        public static void Add(string imagePath, int imageHash, byte[] imgBytes)
        {
            Stream fromFileStream = new MemoryStream(imgBytes); // DO NOT DISPOSE THE STREAM!!! it needs to stay open to access the image.
            Image img = null;

            try
            {
                img = new Bitmap(fromFileStream);
            }
            catch (Exception ex)
            {
                // The image was corrupt or a not support format.
                System.Diagnostics.Debug.WriteLine("TouchClient ImageCache failed to add image '" + imagePath + "' since it was not a valid image.");

                // Use bad image.
                img = Resources.BadImage;
            }

            ImageContainer imgContainer = new ImageContainer(imagePath, imageHash, img, imgBytes.Length);

            // If the image is already in the memory cache then remove it.
            RemoveImage(imagePath);

            // Store the image in the memory cache
            addImageContainerToCache(imgContainer);

            if (AllowHardDriveCache && !_isMasterServerPath && imagePath.StartsWith("IMAGE://", StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    //*******************************************************
                    // Save the image file.
                    //*******************************************************
                    string filePath = Path.Combine(_hardDriveCacheDirectoryPath, imagePath.Substring(8));

                    // Create the directories
                    string directoryPath = Path.GetDirectoryName(filePath);
                    Directory.CreateDirectory(directoryPath);  // If the directory already exists, this method does nothing.
                    File.WriteAllBytes(filePath, imgBytes);
                }
                catch
                {
                    // do nothing. not a huge deal if it failed to save.
                }
            }
        }

    }
}
